﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using System.Xml;
using System.Xml.Linq;
using WpfStarterKit.Framework.ViewModel;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;
using WpfStarterKit.Framework.UnityExtensions;

namespace WpfStarterKit.Framework.Navigation
{
    /// <summary>
    /// This class manages the navigation between pages in the application
    /// </summary>
    public partial class NavigationManager
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="service">An instance of the navigation service passed in by the top-level navigation window/frame</param>
        /// <param name="navigationActionSource">A source from which to obtain view and view model information based on 
        /// navigation actions. Override the GetNavigationMap in an inherited class to change how the this attribute is 
        /// interpreted. The default interpretation assumes that this is a resource name in the entry (main executable
        /// of the current app domain) assembly.</param>
        public NavigationManager(NavigationService service, object navigationActionSource)
        {
            try
            {
                Service = service;
                ViewStack = new Stack<ViewStackEntry>();
                Container = LoadNavigationMap(navigationActionSource);
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        #region Protected Properties

        /// <summary>
        /// A cached instance of the current view's navigation service
        /// </summary>
        protected NavigationService Service { get; private set; }

        /// <summary>
        /// The unity container used to resolve navigation actions to views and view models
        /// </summary>
        protected UnityContainer Container { get; private set; }

        /// <summary>
        /// A reference to the currently loaded view
        /// </summary>
        public FrameworkElement CurrentView { get; private set; }

        /// <summary>
        /// Helper property that returns the current view or overlay's view model
        /// </summary>
        protected BaseViewModel CurrentViewModel
        {
            get
            {
                if (CurrentView != null)
                {
                    return (CurrentView.DataContext as BaseViewModel);
                }

                return null;
            }
        }

        /// <summary>
        /// View model stack maintained to aid in modal navigation. If there's no items in this stack, Back will not work
        /// </summary>
        protected Stack<ViewStackEntry> ViewStack { get; private set; }

        #endregion

        #region Public Properties

        /// <summary>
        /// The currently loaded navigation action (maps to NavigationMap.xml)
        /// </summary>
        public string CurrentNavigationAction { get; private set; }

        /// <summary>
        /// Gets or sets the previous navigation action.
        /// </summary>
        /// <value>The previous navigation action.</value>
        public string PreviousNavigationAction { get; private set; }

        /// <summary>
        /// Whether or not the Back method can be called
        /// </summary>
        public bool CanGoBack
        {
            get
            {
                return (ViewStack.Count > 0);
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Interprets navigationActionSource as a string resource name in the entry (main executable of the current
        /// app domain) assembly and reads it in as XML
        /// </summary>
        /// <param name="navigationActionSource">Text resource that stores the navigation map</param>
        /// <returns>A Unity container with all the views and view model 
        /// types registered</returns>
        public virtual UnityContainer LoadNavigationMap(object navigationActionSource)
        {
            XDocument map = null;

            using (Stream s = Assembly.GetEntryAssembly().GetManifestResourceStream(navigationActionSource as string))
            {
                if (s != null)
                {
                    using (var reader = new XmlTextReader(s))
                    {
                        map = XDocument.Load(reader);
                    }
                }
            }

            var unity = new UnityContainer();

            var navigationActions = from actionTag in map.Descendants(XName.Get("NavigationAction"))
                                    select new
                                    {
                                        Name = actionTag.Attribute(XName.Get("Name")).Value,
                                        ViewTypeName = actionTag.Attribute(XName.Get("View")).Value,
                                        ViewAssemblyName = actionTag.Attribute(XName.Get("ViewAssemblyName")) != null ? actionTag.Attribute(XName.Get("ViewAssemblyName")).Value : null,
                                        ViewModelTypeName = actionTag.Attribute(XName.Get("ViewModel")).Value,
                                        ViewModelAssemblyName = actionTag.Attribute(XName.Get("ViewModelAssemblyName")) != null ? actionTag.Attribute(XName.Get("ViewModelAssemblyName")).Value : null
                                    };

            foreach (var action in navigationActions)
            {
                Assembly viewAssembly = null;
                if (String.IsNullOrEmpty(action.ViewAssemblyName) == false)
                {
                    viewAssembly = Assembly.Load(action.ViewAssemblyName);
                }
                else
                {
                    viewAssembly = Assembly.GetEntryAssembly();
                }

                Type viewType = viewAssembly.GetType(action.ViewTypeName);
                unity.RegisterType(typeof(FrameworkElement), viewType, action.Name);

                Assembly viewModelAssembly = null;
                if (String.IsNullOrEmpty(action.ViewModelAssemblyName) == false)
                {
                    viewModelAssembly = Assembly.Load(action.ViewModelAssemblyName);
                }
                else
                {
                    viewModelAssembly = Assembly.GetEntryAssembly();
                }
                Type viewModelType = viewModelAssembly.GetType(action.ViewModelTypeName);

                var notificationPolicy = unity.AddNewExtension<Interception>()
                    .RegisterType(typeof(BaseViewModel), viewModelType, action.Name)
                    .Configure<Interception>()
                    .SetDefaultInterceptorFor(viewModelType, new VirtualMethodInterceptor())
                    .AddPolicy("NotificationPolicy");

                notificationPolicy.AddMatchingRule(new PropertyMatchingRule("*", PropertyMatchingOption.Set));
                notificationPolicy.AddCallHandler<NotifyPropertyChangedCallHandler>();
            }

            return unity;
        }

        /// <summary>
        /// Navigates to a navigation action called "Start". Inherit the class and override the method to change
        /// this behaviour
        /// </summary>
        public virtual void Start()
        {
            Navigate("Start");
        }

        /// <summary>
        /// Navigates to a view defined by the corresponding navigation action in NavigationMap.xml
        /// </summary>
        /// <param name="navigationAction">Navigation action defined in NavigationMap.xml</param>
        public void Navigate(string navigationAction)
        {
            Navigate(navigationAction, null);
        }

        /// <summary>
        /// Navigates to another view modally. This means that the current view will get its view model back as it was
        /// when the view being navigated to returns.
        /// </summary>
        /// <param name="navigationAction">Navigation action (maps to NavigationMap.xml)</param>
        public void NavigateModal(string navigationAction)
        {
            NavigateModal(navigationAction, true);
        }

        /// <summary>
        /// Navigates to another view modally. This means that the current view will get its view model back as it was
        /// when the view being navigated to returns. The return behavior is also influenced by the journalCurrent
        /// parameter.
        /// </summary>
        /// <param name="navigationAction">Navigation action (maps to NavigationMap.xml)</param>
        /// <param name="journalCurrent">Whether or not to add the current view to the journal. If the current view
        /// isn't added to the journal, calling back on the next view will return to this view's parent</param>
        public void NavigateModal(string navigationAction, bool journalCurrent)
        {
            try
            {
                if (journalCurrent == false && ViewStack.Count == 0)
                {
                    throw new NavigationException("The first view calling a modal view must be journalled");
                }

                if (journalCurrent)
                {
                    var entry = new ViewStackEntry
                    {
                        NavigationAction = CurrentNavigationAction,
                        View = CurrentView
                    };

                    ViewStack.Push(entry);
                }

                NavigateInternal(navigationAction, null);
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        /// <summary>
        /// Used by view models supporting modal views to navigate back to the caller.
        /// </summary>
        public void Back()
        {
            try
            {
                if (ViewStack.Count == 0)
                {
                    throw new NavigationException("You must be in a modal page or an overlay to use this method");
                }

                ViewStackEntry entry = ViewStack.Pop();

                NavigateInternal(entry.NavigationAction, entry.View);
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        /// <summary>
        /// Navigates to a view defined by the corresponding navigation action in NavigationMap.xml
        /// </summary>
        /// <param name="navigationAction">Navigation action defined in NavigationMap.xml</param>
        /// <param name="view">View that corresponds to this navigation action. If a null
        /// is supplied, the NavigationManager will instantiate a new one based on the navigationAction</param>
        private void Navigate(string navigationAction, FrameworkElement view)
        {
            try
            {
                //Unwind the view model stack
                while (ViewStack.Count > 0)
                {
                    RemoveBackEntry();
                }

                NavigateInternal(navigationAction, view);
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        /// <summary>
        /// Remove a back entry. Ensure you check CanGoBack before calling this method.
        /// </summary>
        public void RemoveBackEntry()
        {
            try
            {
                ViewStackEntry entry = ViewStack.Pop();
                UserInterfaceViewModel uiViewModel = entry.View.DataContext as UserInterfaceViewModel;
                if (uiViewModel != null)
                {
                    uiViewModel.RemovedFromStack(CurrentNavigationAction);
                }
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        /// <summary>
        /// Enable input controls on the current view. This method call should follow DisableInput (eventually)
        /// </summary>
        public void EnableInput()
        {
            EnablePageInput();
        }

        /// <summary>
        /// Disable input controls on the current view. This method should be called when performing a 
        /// long running operation
        /// </summary>
        public void DisableInput()
        {
            DisablePageInput();
        }

        #endregion

        #region Event Handlers

        void ServiceLoadCompleted(object sender, NavigationEventArgs e)
        {
            try
            {
                Service.LoadCompleted -= ServiceLoadCompleted;
                Service.RemoveBackEntry();
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        void CurrentViewLoaded(object sender, RoutedEventArgs e)
        {
            try
            {
                var userInterfaceViewModel = CurrentView.DataContext as UserInterfaceViewModel;
                if (userInterfaceViewModel != null)
                {
                    userInterfaceViewModel.MarkInitialized(new EventArgs());
                }
                EnableInput();
            }
            catch (ThreadAbortException threadException)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(threadException, "NSCorp.Rit.UI.App.Managers.NavigationManager", false, GetType());
            }
            catch (Exception exception)
            {
                ApplicationManager.Instance.ExceptionHandler.HandleException(exception, "NSCorp.Rit.UI.App.Managers.NavigationManager", true, GetType());
            }
        }

        #endregion

        #region Private Methods

        void EnablePageInput()
        {
            Page page = CurrentView as Page;
            if (page != null)
            {
                page.IsEnabled = true;
            }
        }

        void DisablePageInput()
        {
            Page page = CurrentView as Page;
            if (page != null)
            {
                page.IsEnabled = false;
            }
        }

        void NavigateInternal(string navigationAction, FrameworkElement view)
        {
            var sleeper = CurrentViewModel as ISleep;
            IProvideData dataProvider = null;
            string oldAction = CurrentNavigationAction;
            PreviousNavigationAction = oldAction;
            UserInterfaceViewModel oldUiViewModel = null;

            //Disable all input controls before navigating
            DisableInput();

            if (CurrentView != null)
            {
                NavigationService currentService = NavigationService.GetNavigationService(CurrentView);
                if (currentService != null)
                {
                    Service = currentService;
                }

                if (CurrentView.DataContext != null && (CurrentView.DataContext as UserInterfaceViewModel) != null)
                {
                    CurrentView.Loaded -= CurrentViewLoaded;
                    dataProvider = CurrentView.DataContext as IProvideData;
                    oldUiViewModel = CurrentView.DataContext as UserInterfaceViewModel;
                }
            }

            BaseViewModel viewModel = null;
            if (view == null)
            {
                CurrentView = Container.Resolve<FrameworkElement>(navigationAction);
                viewModel = Container.Resolve<BaseViewModel>(navigationAction);
            }
            else
            {
                CurrentView = view;
                viewModel = view.DataContext as BaseViewModel;
            }

            CurrentNavigationAction = navigationAction;

            IConsumeData dataConsumer = viewModel as IConsumeData;
            UserInterfaceViewModel uiViewModel = viewModel as UserInterfaceViewModel;

            if (dataProvider != null && dataConsumer != null)
            {
                dataConsumer.ConsumeData(oldAction, dataProvider.ProvideData(navigationAction));
            }

            if (oldUiViewModel != null && uiViewModel != null)
            {
                uiViewModel.ConsumeValues(oldAction, oldUiViewModel.ProvideValues(navigationAction));
            }

            if (sleeper != null)
            {
                sleeper.Sleep();
            }

            if (CurrentView != null)
            {
                if (CurrentView.DataContext == null)
                {
                    CurrentView.DataContext = viewModel;
                }

                if (uiViewModel != null)
                {
                    CurrentView.Loaded += CurrentViewLoaded;
                }
            }

            if (uiViewModel != null)
            {
                LongRunningOperationAttribute wait = LongRunningOperationAttribute.GetAttribute(uiViewModel.GetType().GetMethod("Initialize"));

                if (wait != null)
                {
                    LongRunningOperationExecutionManager longRunningOperation = new LongRunningOperationExecutionManager(wait, new Action(uiViewModel.Initialize), null);
                    longRunningOperation.Executed += new EventHandler(longRunningOperation_Executed);
                    longRunningOperation.Execute();
                }
                else
                {
                    uiViewModel.Initialize();
                    uiViewModel.CallReturnMethods(PreviousNavigationAction);
                    NavigateToCurrentView();
                }
            }
            else
            {
                NavigateToCurrentView();
            }
        }

        void NavigateToCurrentView()
        {
            if (Service != null)
            {
                Service.LoadCompleted += ServiceLoadCompleted;
                Service.Navigate(CurrentView);
            }
        }

        void longRunningOperation_Executed(object sender, EventArgs e)
        {
            var uiViewModel = CurrentViewModel as UserInterfaceViewModel;
            if (uiViewModel != null)
            {
                uiViewModel.CallReturnMethods(PreviousNavigationAction);
            }

            NavigateToCurrentView();
        }

        #endregion
    }
}
